//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

package mediaconvert

import (
	"context"
	"fmt"
	"io"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/pkg/errors"

	"github.com/hcengineering/stream/internal/pkg/log"
	"github.com/hcengineering/stream/internal/pkg/manifest"
	"github.com/hcengineering/stream/internal/pkg/profile"
	"go.uber.org/zap"
)

// LogLevel is ffmpeg log level
type LogLevel string

const (
	// LogLevelQuiet is quiet log level
	LogLevelQuiet LogLevel = "quiet"
	// LogLevelPanic is panic log level
	LogLevelPanic LogLevel = "panic"
	// LogLevelFatal is fatal log level
	LogLevelFatal LogLevel = "fatal"
	// LogLevelError is error log level
	LogLevelError LogLevel = "error"
	// LogLevelWarning is warning log level
	LogLevelWarning LogLevel = "warning"
	// LogLevelInfo is info log level
	LogLevelInfo LogLevel = "info"
	// LogLevelVerbose is verbose log level
	LogLevelVerbose LogLevel = "verbose"
	// LogLevelDebug is debug log level
	LogLevelDebug LogLevel = "debug"
	// LogLevelTrace is trace log level
	LogLevelTrace LogLevel = "trace"
)

// Options represents configuration for the ffmpeg command
type Options struct {
	Input     string
	OutputDir string
	LogLevel  LogLevel
	Threads   int
	UploadID  string
	Profiles  []profile.VideoProfile
}

func newFfmpegCommand(ctx context.Context, in io.Reader, args []string) (*exec.Cmd, error) {
	if ctx == nil {
		return nil, errors.New("ctx should not be nil")
	}

	var cmd = exec.CommandContext(ctx, "ffmpeg", args...)
	cmd.Stdin = in

	var logger = log.FromContext(ctx).With(zap.String("func", "newFFMpegCommand"))
	logger.Debug("prepared command: ", zap.String("cmd", cmd.String()))

	return cmd, nil
}

func buildCommonCommand(opts *Options) []string {
	var result = []string{
		"-y", // Overwrite output files without asking.
		"-v", string(opts.LogLevel),
		"-err_detect", "ignore_err",
		"-fflags", "+discardcorrupt",
		"-threads", fmt.Sprint(opts.Threads),
		"-i", opts.Input,
	}

	// If input is a URL, add HTTP specific parameters
	if strings.HasPrefix(opts.Input, "http://") || strings.HasPrefix(opts.Input, "https://") {
		result = append(result,
			"-reconnect", "1",
			"-reconnect_streamed", "1",
			"-reconnect_delay_max", "5",
		)
	}

	return result
}

func buildHLSCommand(profile profile.VideoProfile, opts *Options) []string {
	return []string{
		"-f", "hls",
		"-hls_time", "5",
		// Use HLS flags
		// - split_by_time
		//     Allow segments to start on frames other than key frames.
		//     This improves behavior on some players when the time between key frames is inconsistent,
		//     but may make things worse on others, and can cause some oddities during seeking.
		//     This flag should be used with the hls_time option.
		// - temp_file
		//     Write segment data to filename.tmp and rename to filename only once the segment is complete.
		"-hls_flags", "split_by_time+temp_file",
		// Do not limit number of HLS segments
		"-hls_list_size", "0",
		"-hls_segment_filename", filepath.Join(opts.OutputDir, opts.UploadID, fmt.Sprintf("%s_%s_%s.ts", opts.UploadID, "%03d", profile.Name)),
	}
}

func buildVideoCommand(profile profile.VideoProfile) []string {
	crf := profile.CRF
	if crf == 0 {
		crf = 23
	}
	command := []string{
		// Transcode only first video and optionally audio stream
		"-map", "0:v:0",
		"-map", "0:a?",
		// Set up codecs
		"-c:a", profile.AudioCodec,
		"-c:v", profile.VideoCodec,
		"-preset", "veryfast",
		"-crf", strconv.Itoa(crf),
		"-g", "60",
	}

	if profile.VideoCodec != "copy" && profile.Scale {
		command = append(command, "-vf", "scale=-2:"+strconv.Itoa(profile.Height))
	}

	return command
}

// BuildAudioCommand returns flags for getting the audio from the input
func BuildAudioCommand(opts *Options) []string {
	var commonPart = buildCommonCommand(opts)

	return append(commonPart,
		"-vn", "-acodec",
		"copy", filepath.Join(opts.OutputDir, opts.UploadID),
	)
}

// BuildVideoCommand returns ffmpeg command for converting video.
func BuildVideoCommand(opts *Options) []string {
	if len(opts.Profiles) == 0 {
		return []string{}
	}

	var command = buildCommonCommand(opts)
	for _, profile := range opts.Profiles {
		command = append(command, buildVideoCommand(profile)...)
		command = append(command, buildHLSCommand(profile, opts)...)
		command = append(command, filepath.Join(opts.OutputDir, opts.UploadID, manifest.PlaylistFileName(opts.UploadID, profile.Name)))
	}
	return command
}

// BuildThumbnailCommand creates a command that creates a thumbnail for the input video
func BuildThumbnailCommand(opts *Options) []string {
	return append([]string{},
		"-y", // Overwrite output files without asking.
		"-i", opts.Input,
		"-vframes", "1",
		"-update", "1",
		filepath.Join(opts.OutputDir, opts.UploadID, opts.UploadID+".jpg"),
	)
}
